Change creator_id to only need to be unique for schools where rejecte…#742
Conversation
Test coverage89.63% line coverage reported by SimpleCov. |
There was a problem hiding this comment.
Pull request overview
This PR updates the School model so a creator_id only needs to be unique among non-rejected schools, enabling a user to create a new school after their previous one was rejected.
Changes:
- Scope
creator_iduniqueness validation to records whererejected_atisnil.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def change | ||
| remove_index :schools, name: "index_schools_on_creator_id" |
There was a problem hiding this comment.
remove_index is called by name: inside a change migration, which makes the migration irreversible on rollback (Rails can’t reconstruct the removed index definition from the name alone). Consider switching to an explicit up/down (or reversible) that re-adds the original unique index on creator_id in down, and/or use remove_index :schools, :creator_id so Rails can invert it.
| :creator_id, | ||
| unique: true, | ||
| where: "rejected_at IS NULL", | ||
| name: "index_schools_on_creator_id_active_only" |
There was a problem hiding this comment.
The new partial index is given a non-standard name (index_schools_on_creator_id_active_only). For consistency with the existing partial unique indexes on schools (e.g. index_schools_on_reference), consider reusing the conventional name index_schools_on_creator_id for the replacement index to reduce future migration complexity.
| name: "index_schools_on_creator_id_active_only" | |
| name: "index_schools_on_creator_id" |
There was a problem hiding this comment.
I don't have a preference either way. I don't see any problem with the more descriptive name as you only tend to refer to index names from within other migrations. I think there is a limit to the name length but this would likely be within it.
I would choose what you think is clearer.
| validates :creator_id, | ||
| presence: true, | ||
| uniqueness: { | ||
| conditions: -> { where(rejected_at: nil) } | ||
| } |
There was a problem hiding this comment.
Test coverage: this changes creator_id uniqueness semantics to allow reuse when the existing school is rejected. Please add/adjust specs to cover the new behavior (duplicate creator_id allowed when the other record has rejected_at set, but still rejected when both are active).
| def self.find_for_user!(user) | ||
| school = Role.find_by(user_id: user.id)&.school || find_by(creator_id: user.id) | ||
| school = Role.find_by(user_id: user.id)&.school || find_by(creator_id: user.id, rejected_at: nil) | ||
| raise ActiveRecord::RecordNotFound unless school |
There was a problem hiding this comment.
Test coverage: .find_for_user! now excludes schools created by the user if they are rejected (rejected_at set). Add a spec for the rejected-school case (e.g., creator has only a rejected school -> RecordNotFound, and creator has both rejected + active -> returns the active one).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| remove_index :schools, name: "index_schools_on_creator_id_active_only" | ||
|
|
||
| add_index :schools, | ||
| :creator_id, | ||
| unique: true, | ||
| name: "index_schools_on_creator_id" |
There was a problem hiding this comment.
The down migration recreates a global unique index on creator_id. After this change is deployed, the table may legitimately contain multiple rows with the same creator_id (as long as only one is active), so rolling back would fail when re-adding this index. Consider marking the migration as irreversible, or adding a rollback strategy (e.g., clean up/merge duplicate rows or raise with a clear message before attempting to add the unique index).
| remove_index :schools, name: "index_schools_on_creator_id_active_only" | |
| add_index :schools, | |
| :creator_id, | |
| unique: true, | |
| name: "index_schools_on_creator_id" | |
| raise ActiveRecord::IrreversibleMigration, | |
| "Cannot safely restore global unique index on creator_id because " \ | |
| "multiple rows per creator_id may exist after this migration." |
There was a problem hiding this comment.
This risk here is that if we did roll back, and the remove_index succeeded, but the add_index didn't, we would could get duplicates.
This is a small risk as it only applies if we decide to manually roll back the code and migrations so I don't have a strong preference as to if it's worth changing or not - up to you.
| def self.find_for_user!(user) | ||
| school = Role.find_by(user_id: user.id)&.school || find_by(creator_id: user.id) | ||
| school = Role.find_by(user_id: user.id)&.school || find_by(creator_id: user.id, rejected_at: nil) | ||
| raise ActiveRecord::RecordNotFound unless school |
There was a problem hiding this comment.
This change introduces new behavior (creator_id can be reused when the existing school is rejected, and .find_for_user! should ignore rejected schools). There are existing model specs for creator_id uniqueness and .find_for_user! (spec/models/school_spec.rb), but they don't appear to cover the rejected-school cases. Please add/adjust tests to assert that a second school with the same creator_id is valid when the first has rejected_at set, and that .find_for_user! returns the active school (or raises) when only a rejected school exists.
Status
What's changed?
creator_idcan be reused ifrejected_atis not nil